Skip to content

feat(sim-mailer): email inbox for mothership with chat history and plan gating#3558

Merged
waleedlatif1 merged 21 commits intofeat/mothership-copilotfrom
fix/sim-mailer
Mar 13, 2026
Merged

feat(sim-mailer): email inbox for mothership with chat history and plan gating#3558
waleedlatif1 merged 21 commits intofeat/mothership-copilotfrom
fix/sim-mailer

Conversation

@waleedlatif1
Copy link
Collaborator

@waleedlatif1 waleedlatif1 commented Mar 13, 2026

Summary

Adds the Sim Mailer feature — a complete email inbox system that allows users to email tasks to their workspace and have the mothership process them. This PR includes the full backend pipeline, settings UI, and several critical fixes for multi-turn email conversations.

Core Backend

  • AgentMail webhook handler (app/api/webhooks/agentmail/route.ts) — receives inbound emails, validates senders, creates inbox tasks, and queues background execution via Trigger.dev
  • Inbox executor (lib/mothership/inbox/executor.ts) — resolves user identity, manages chat lifecycle, runs the mothership orchestrator, persists messages, and sends response emails
  • Email formatting (lib/mothership/inbox/format.ts) — strips quoted replies to avoid duplication, formats emails as mothership-compatible messages with attachment metadata
  • Response handling (lib/mothership/inbox/response.ts) — sends formatted HTML response emails via AgentMail
  • AgentMail client (lib/mothership/inbox/agentmail-client.ts) — typed wrapper around the AgentMail API for mailbox/message operations
  • Inbox lifecycle (lib/mothership/inbox/lifecycle.ts) — mailbox provisioning, webhook registration, address updates, and teardown

Chat History Fix

  • Changed executor payload from messages: [{ role: 'user', content }] (array) to message: content (singular string) to match the interactive copilot flow
  • Verified against the Go backend: when message (singular) is sent with a chatId, the Go service loads the full conversation history from the chats table via repository.Load(). The previous messages array approach caused Go to extract only the last message and discard history context
  • This ensures multi-turn email conversations work correctly — reply chains maintain full context

Settings UI

  • Inbox settings page (settings/components/inbox/) with five components:
    • inbox.tsx — main component with Max plan gating and upgrade prompt
    • inbox-enable-toggle.tsx — toggle to enable/disable the inbox with mailbox provisioning modal
    • inbox-settings-tab.tsx — email address display (read-only input with copy/edit tooltips matching sub-block pattern), allowed senders management
    • inbox-task-list.tsx — searchable, filterable task list with status badges, relative timestamps, and clickable tasks that navigate to /workspace/{id}/task/{chatId}
    • inbox-skeleton.tsx — loading skeletons matching the actual layout structure
  • Search bar styling matches the Copilot Keys search bar (uses Input from @/components/ui)
  • Copy/edit icons use the same pattern as sub-block copy buttons (h-3 w-3 text-muted-foreground with Tooltips)

Plan Gating

  • Sim Mailer requires a Max plan (getPlanTierCredits(plan) >= 25000 || isEnterprise(plan))
  • Non-Max users see an upgrade prompt with a button linking to the subscription page
  • Subscription data is only fetched when billing is enabled (useSubscriptionData({ enabled: isBillingEnabled }))

Settings Infrastructure

  • Added scrollbar-gutter: stable to the settings layout to prevent content shift when filtering changes page height
  • Registered Inbox as a dynamic import in settings.tsx with InboxSkeleton loading fallback
  • Added inbox to SettingsSection type and navigation items with Mail icon

Database

  • Added inbox_enabled, inbox_address, inbox_provider_id columns to workspace table
  • Added mothership_inbox_task table with indexes on (workspace_id, created_at), (workspace_id, status), email_message_id, and response_message_id
  • Added mothership_inbox_allowed_sender table with unique index on (workspace_id, email)
  • Added mothership_inbox_webhook table for webhook secret management
  • Migration: 0172_glossy_miek.sql

Feature Flags

  • INBOX_ENABLED (server) + NEXT_PUBLIC_INBOX_ENABLED (client) following the established dual env var pattern (same as SSO, credential sets, access control)
  • hasInboxAccess() server-side check in lib/billing/core/subscription.ts

Bug Fix

  • Fixed chatId variable scoping in executor.ts catch block — was declared inside try but referenced in catch, causing error response emails to lose chat association

React Query

  • hooks/queries/use-inbox.ts — query key factory with inboxKeys, hooks for config, tasks, senders, and mutations for toggle, address update, sender add/remove
  • All queries have explicit staleTime, signal forwarding, and proper cache invalidation

Type of Change

  • New feature
  • Bug fix (chat history, chatId scoping)

Testing

Tested manually — sent emails, verified multi-turn conversation context, tested plan gating, verified task navigation to chat view, tested search/filter, copy/edit tooltips, scrollbar stability.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 13, 2026 10:12am

Request Review

@cursor
Copy link

cursor bot commented Mar 13, 2026

PR Summary

High Risk
Adds a new email-to-task execution pipeline (webhooks, background processing, outbound replies) plus new DB tables/columns and feature-flag/billing gating, which is inherently security- and reliability-sensitive. Incorrect verification/allowlisting/rate limiting or task execution behavior could lead to unauthorized task creation or noisy/failed processing.

Overview
Adds Sim Mailer (email inbox for mothership tasks). New AgentMail webhook endpoint verifies Svix signatures, de-dupes messages, enforces allowed-sender + automated-sender rejection + per-workspace rate limiting, persists mothershipInboxTask rows, and dispatches execution via Trigger.dev with local fallback.

Implements end-to-end inbox execution and replies. Adds an inbox executor that claims tasks, resolves/creates chats, runs the orchestrator with persisted chat messages, optionally fetches attachment metadata, and replies via AgentMail with rendered HTML; includes inbox lifecycle helpers to provision/teardown inboxes + webhooks.

Exposes configuration and UI with plan/flag gating. Adds workspace APIs to enable/disable/regenerate inbox addresses, manage allowed senders, and list inbox tasks; adds a new Settings inbox section with enable toggle, address management, sender management, and task list. Introduces hasInboxAccess() (Max/enterprise or env override), new env flags (INBOX_ENABLED, NEXT_PUBLIC_INBOX_ENABLED), adjusts isHosted detection, and ships DB migrations for inbox tables plus workspace inbox columns (and adds marked + svix dependencies).

Written by Cursor Bugbot for commit 4997216. Configure here.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR introduces Sim Mailer — a full email-to-task pipeline that lets workspace members send emails to an AgentMail-provisioned inbox, have the Mothership orchestrator process them, and receive a formatted reply. The feature includes the complete backend pipeline (webhook handler, executor, lifecycle, response), a settings UI with plan gating, React Query hooks, database schema, and a Trigger.dev background task.

The implementation is thorough and incorporates many fixes identified in prior review rounds — Svix HMAC signature verification, atomic task claiming, JSONB-level chat message appends, enum-validated status filters, cursor validation, HTML sanitisation with code-fence awareness, javascript: URI stripping, idempotency guards, and consistent 401 responses for all unauthenticated webhook paths.

Remaining issue:

  • resolveUserId in executor.ts uses eq(user.email, senderEmail.toLowerCase()) — a case-sensitive SQL comparison. If a workspace member's email was stored with uppercase characters (e.g. by an OAuth provider), the lookup silently falls back to ws.ownerId, causing the task to execute under the workspace owner's identity rather than the sender's. The identical fix already applied to isSenderAllowed in the webhook route (sql\lower(${user.email}) = ${email}``) should be applied here too.

Minor:

  • agentmail-client.ts fetch calls have no request timeout; a slow or unresponsive AgentMail API can hang the executor until Trigger.dev's machine wall-clock limit is hit.

Confidence Score: 4/5

  • Safe to merge after fixing the case-sensitive email lookup in resolveUserId; the one remaining logic bug causes incorrect user identity attribution rather than data loss or security failure.
  • The PR addresses a large number of security and correctness issues identified in prior review rounds — signature verification, atomic claiming, duplicate prevention, sanitisation, rate-limit accuracy, and plan enforcement are all solid. The one outstanding logic bug (resolveUserId case-sensitivity) is a correctness issue that silently degrades behaviour for mixed-case-email senders without breaking the feature entirely. All other changes are well-structured.
  • apps/sim/lib/mothership/inbox/executor.ts (resolveUserId case-sensitive email comparison)

Important Files Changed

Filename Overview
apps/sim/app/api/webhooks/agentmail/route.ts AgentMail webhook handler with Svix HMAC-SHA256 signature verification, sender allowlisting, rate limiting (excluding rejected tasks), deduplication on emailMessageId, and consistent 401 responses for all unauthenticated paths.
apps/sim/lib/mothership/inbox/executor.ts Inbox task executor with atomic claim, idempotency guard, atomic JSONB chat-message append, and proper responseSent flag. However, resolveUserId uses a case-sensitive SQL eq() on user.email instead of lower(), meaning mixed-case emails fall back to the workspace owner identity.
apps/sim/lib/mothership/inbox/lifecycle.ts Inbox lifecycle (enable/disable/update address) with rollback that also deletes the orphaned mothershipInboxWebhook row on partial failure. The disable-then-enable approach for address updates is documented as an accepted MVP limitation.
apps/sim/lib/mothership/inbox/response.ts Email response formatter with stripRawHtml (code-fence-aware), stripUnsafeUrls (removes javascript:/vbscript:/data: hrefs), and proper HTML escaping for user-controlled values in rendered HTML.
apps/sim/lib/mothership/inbox/agentmail-client.ts Typed AgentMail API wrapper. All fetch calls lack a request timeout, which could cause the executor to hang indefinitely if AgentMail is unresponsive.

Sequence Diagram

sequenceDiagram
    participant Sender as Email Sender
    participant AM as AgentMail
    participant WH as Webhook Handler<br/>/api/webhooks/agentmail
    participant DB as Database
    participant TD as Trigger.dev
    participant EX as Inbox Executor
    participant GO as Go Mothership<br/>/api/mothership/execute
    participant RE as Response Sender

    Sender->>AM: Send email
    AM->>WH: POST webhook (Svix HMAC-SHA256)
    WH->>DB: Lookup workspace by inbox_id + webhook secret
    WH->>WH: Verify Svix signature
    WH->>DB: Check duplicate (emailMessageId)
    WH->>DB: isSenderAllowed (lower(user.email))
    WH->>DB: getRecentTaskCount (excl. rejected)
    WH->>DB: Lookup parent chatId (via responseMessageId)
    WH->>DB: INSERT mothershipInboxTask (status=received)
    WH->>TD: tasks.trigger(mothership-inbox-execution)
    TD->>EX: executeInboxTask(taskId)
    EX->>DB: Early exit if completed/failed
    EX->>DB: Atomic claim (UPDATE WHERE status=received)
    EX->>DB: resolveUserId (senderEmail → workspace member)
    EX->>EX: formatEmailAsMessage
    EX->>GO: orchestrateCopilotStream
    GO-->>EX: OrchestratorResult
    EX->>DB: JSONB append chat messages (atomic)
    EX->>RE: sendInboxResponse
    RE->>AM: replyToMessage
    AM-->>Sender: Response email
    EX->>DB: UPDATE task (status=completed, responseMessageId)
Loading

Comments Outside Diff (2)

  1. apps/sim/lib/mothership/inbox/executor.ts, line 1127 (link)

    Case-sensitive email comparison in resolveUserId

    resolveUserId uses eq(user.email, senderEmail.toLowerCase()), which is a case-sensitive SQL equality comparison. If a workspace member's email was stored with any uppercase characters (e.g. User@Example.com), the lookup will fail and the function will fall back to ws.ownerId. As a result, the task executes under the workspace owner's identity rather than the actual sender's.

    This is the same bug that was already fixed in isSenderAllowed (webhook route) by switching to sql\lower(${user.email}) = ${email}``. The same fix is needed here:

  2. apps/sim/lib/mothership/inbox/agentmail-client.ts, line 730-756 (link)

    No request timeout on AgentMail API calls

    All fetch calls to the AgentMail API (both request and requestRaw) are issued without a timeout. If AgentMail is slow or unresponsive, the inbox executor will hang indefinitely — potentially exhausting the Trigger.dev machine's wall-clock limit without a clean error or retry signal.

    Consider adding an AbortSignal with a reasonable timeout (e.g. 30 s):

    async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
      const url = `${BASE_URL}${path}`
      const controller = new AbortController()
      const timeout = setTimeout(() => controller.abort(), 30_000)
      try {
        const response = await fetch(url, {
          ...options,
          signal: controller.signal,
          headers: { ... },
        })
        // ...
      } finally {
        clearTimeout(timeout)
      }
    }

Last reviewed commit: 8b6a3b3

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Collaborator Author

@greptile

waleedlatif1 and others added 13 commits March 13, 2026 02:48
- Exclude rejected tasks from rate limit count to prevent DoS via spam
- Strip raw HTML from LLM output before marked.parse to prevent XSS in emails
- Track responseSent flag to prevent duplicate emails when DB update fails after send
- Use dynamic isHosted from feature-flags instead of hardcoded true
- Atomic JSON append for chat message persistence (eliminates read-modify-write race)
- Handle cutIndex === 0 in stripQuotedReply (body starts with quote)
- Clean up orphan mothershipInboxWebhook row on enableInbox rollback
- Validate status query parameter against enum in tasks API
…ping

- Validate cursor date before using in query (return 400 for invalid)
- Split on fenced code blocks before stripping HTML tags to preserve
  code examples in email responses
…emove-sender errors

- Guard against null result.content in stripThinkingTags
- Use encodeURIComponent on all AgentMail API path parameters
- Surface handleRemoveSender errors to the user instead of swallowing
…ptimistic ID collision

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nt flag, prevent inbox enumeration

- Replace manual HMAC-SHA256 verification with official Svix library per AgentMail docs
- Fix responseSent flag: only set true when email delivery actually succeeds
- Return consistent 401 for unknown inbox and bad signature to prevent enumeration
- Make AgentMailInbox.organization_id optional to match API docs
…→ 0173)

Sync schema with target branch and regenerate migration as 0173
to avoid conflicts with 0172_silky_magma on feat/mothership-copilot.
… divergence

Target branch added 0172_silky_magma, so our inbox migration is now 0173_youthful_stryfe.
@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

…s in email HTML

- Use lower() in isSenderAllowed SQL to match workspace members regardless
  of email case stored by auth provider
- Strip javascript:, vbscript:, and data: URIs from marked HTML output to
  prevent XSS in outbound email responses
@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Consistent with the isSenderAllowed fix — uses lower() so mixed-case
stored emails match correctly, preventing silent fallback to workspace owner.
@waleedlatif1
Copy link
Collaborator Author

@cursor review

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@waleedlatif1 waleedlatif1 merged commit 33bb01b into feat/mothership-copilot Mar 13, 2026
4 checks passed
@waleedlatif1 waleedlatif1 deleted the fix/sim-mailer branch March 13, 2026 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant